package Script;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import Calculate.ByteCalc;
import Error.InsertError;
import Error.StandardError;
import Pointers.JPointerFactory;
import Pointers.JPointerFactory.PointerType;

import interfaces.ErrorReport;
import interfaces.Pointer;
import interfaces.PointerReader;
import interfaces.ScriptInserter;
import interfaces.ScriptReader;
import interfaces.TextScriptArea;
import interfaces.ErrorReport.ErrorType;
import Language.Language;

/**
 * Inserts a script file into a gamefile.
 * 
 * @author Tobias Edin
 */
public class JScriptInserter implements ScriptInserter {
	private File gamefile;
	private File pointerfile;
	private long currentFileOffset;
	
	public JScriptInserter(File gamefile) {
		this.gamefile = gamefile;
		this.pointerfile = gamefile;
		this.currentFileOffset = 0;
	}
	
	public JScriptInserter(File gamefile, File pointerfile) {
		this.gamefile = gamefile;
		this.pointerfile = pointerfile;
		this.currentFileOffset = 0;
	}
	

	@Override
	public ArrayList<ErrorReport> insert(Offset textStart, Offset textEnd, 
										 File script,
										 long header,
										 ArrayList<Offset> pOffsets, 
										 PointerType pType,
										 JTableFile table,
										 int fixedLength, byte padByte) 
										 throws FileNotFoundException,
										 		IOException {
		
		ArrayList<ErrorReport> 			err = new ArrayList<ErrorReport>();
		ScriptReader 					scriptReader = new JScriptReader(script, table.getCharset());
		HashMap<Integer, Pointer>		pointers;
		ArrayList<Integer>				textAreaPointers;
		ArrayList<TextScriptArea> 		text;
		RandomAccessFile 				file;
		Pointer 						tempPoint;
		PointerReader 					pointerReader = 
			new JPointerReader(gamefile, pOffsets, new JPointerFactory(pType));	
		
		/* Validate pointer data. */
		err.addAll(pointerReader.isValid());
		if (!validate(err)) {
			return err;
		}
		
		/* Check tablefile. */
		if (table == null) {
			err.add(new StandardError(Language.noTable, ErrorType.ERROR));
			return err;
		}
		
		/* Set up filewriter. */
		if (gamefile.exists()) {
			file = new RandomAccessFile(gamefile, "rwd");
		
		/* If the gamefile no longer exists: */
		} else {
			err.add(new StandardError(Language.noGamefile, ErrorType.ERROR));
			return err;
		}
		
		/* 
		 * Get text objects from script, and pointers from pointer table.
		 * Throws exception if file cannot be read. 
		 */
		try {
			pointers = pointerReader.getPointers();
			text = scriptReader.read(fixedLength);
		} catch (NumberFormatException e) {
			file.close();
			throw e;
		}
		
		/* Validate the script file. */
		err.addAll(scriptReader.isValid());
		if (!validate(err)) {
			return err;
		}
		
		/*
		 * For each text area, write it to the file, and repoint every
		 * pointer associated with it to the offset where the writing
		 * began.
		 */
		currentFileOffset = textStart.getValue();
		
		for (TextScriptArea t : text) {
			
			/* 
			 * Point all belonging pointer here before 
			 * file offset is updated. Skipped if
			 * there aren't any pointers.
			 */
			textAreaPointers = t.getPointers();
			
			for (Integer tap : textAreaPointers) {
				tempPoint = pointers.get(tap);
				
				/* 
				 * Check if the script defines a pointer that hasn't 
				 * been read.
				 */
				if (tempPoint == null) {
					err.add(new StandardError(Language.missingPointer + 
							" " + tap, 
							ErrorType.WARNING));
				} else {
					
					/* Update existing pointers value. */
					tempPoint.setValue(new Offset(currentFileOffset - header));
				}
			}
			
			/* 
			 * Print the text to the file and
			 * get the error report. 
			 */
			err = processTextArea(table, t, file, textEnd, 
								  fixedLength, padByte);
			
			/* If there are critical errors, abort. */
			if (!validate(err)) {
				file.close();
				return err;
			}
		}
		
		/* If there's space left in the print range, fill it with zeros. */
		while (currentFileOffset < textEnd.getValue()) {
			file.write(0);
			currentFileOffset++;
		}
		
		/* Write the updated pointer values to the gamefile. */
		JPointerWriter.write(pointerfile, pointers);
		
		file.close();
		return err;
	}
	
	/**
	 * Prints the text from a TextScriptArea into a file.
	 * @param table 			Tablefile used for text cnversion.
	 * @param t 				Text area.
	 * @param file 				File to write to.
	 * @param currentFileOffset Offset to start writing from.
	 * @param stopOffset 		The stop position for writing the script in 
	 * 							the file. Used to determine if the insert 
	 * 							range is too small for this script. 
	 * 							Trying to write outside this range counts 
	 * 							as a critical error.
	 * @param fixedLength		Length of fixed-length text entrie. 0 if
	 * 							fixed length isn't used.
	 * @param pad				Byte value that strings shorter than fixedlenth
	 * 							should be padded with.
	 * @return List of errors occured.
	 */
	private ArrayList<ErrorReport> processTextArea(JTableFile table, 
												   TextScriptArea t,
												   RandomAccessFile file,
												   Offset stop,
												   int fixedLength, byte pad) {
		ArrayList<ErrorReport> err = new ArrayList<ErrorReport>();
		ArrayList<String> missingChars = new ArrayList<String>();
		
		/* Printing variables. */
		String							pText;
		String							submitText;
		byte[]							tableData;
		int								hexEntryLength = 5;
		
		/* Fixed length variables. */
		int								bytesWritten = 0;
		byte[]							flData = new byte[]{pad};
		
		/* Get the text from the text script area. */
		pText = t.getText();
		
		/* 
		 * If fixed length is being used, make sure the string
		 * isn't longer than allowed. In a fexed length dump,
		 * each n-length string will recide in its own
		 * TextScriptArea.
		 */
		if (fixedLength > 0 && pText.length() > fixedLength) {
			pText = pText.substring(0, fixedLength);
			table.removeValues(2);
		}
		
		/*
		 * For each text area, probe downwards for the largest
		 * possible match in the table file, print its bytes,
		 * then move the offest forward until the entire string
		 * has been printed.
		 */
		
		/* 
		 * Start at the longest text length, or the print-text length
		 * if it is lower than the longest table text entry.
		 */
		int i = Math.min(pText.length(), table.getTextMaxLength());
		if (i < hexEntryLength && pText.length() >= hexEntryLength) {
			i = hexEntryLength;
		}
		while(pText.length() > 0) {
			
			/* 
			 * Take the i:th first characters and check
			 * the table for them. If there's less characters
			 * left in the string than mex text length, that
			 * value is chosen instead.
			 */
			submitText = pText.substring(0, i);
			tableData = table.getBytes(submitText);
			
			/* 
			 * If there's no tablefile match, check if this is hex entry. 
			 * All hex entries (<$XX>) are of length 5.
			 */
			if (tableData == null && submitText.length() == hexEntryLength) {
				tableData = hexEntryToBytes(submitText);
			}
			
			/* Print any matches and move text offset forward. */
			if (tableData != null) {
				
				if (!print(file, tableData, currentFileOffset, stop.getValue())) {
					err.add(new StandardError(
							Language.writeError, ErrorType.ERROR));
					return err;
					
				} else if (fixedLength > 0) {
					
					/* 
					 * If fixed length is used, mark one additional
					 * byte as written.
					 */
					bytesWritten++;
				}
				
				/* 
				 * If this chunk of data goes outside the print range,
				 * signal error.
				 */
				if (currentFileOffset + tableData.length > stop.getValue()) {
					err.add(new StandardError(
							Language.outsidePrintrange, ErrorType.ERROR));
					return err;
				} else {
					
					/* 
					 * Update the file offset. Must be done after
					 * the file offset + data check, to avoid
					 * errors on the last character.
					 */
					currentFileOffset += tableData.length;
				}
				
				/* Remove printed text from string. */
				pText = pText.substring(i, pText.length());
			
			}
			
			/* 
			 * If no match is found for a single char in the text,
			 * skip writing it and file a warning.
			 */
			if (i == 1 && tableData == null) {
				
				/* Save the missing characters. */
				missingChars.add(pText.substring(0, i));
				
				/* Remove the foremost character in the text. */
				if (pText.length() > 1) {
					pText = pText.substring(1, pText.length());
					
				} else {
					
					/* If this is the last character, terminate loop. */
					pText = "";
				}
				
			} else if (i >= 1 && tableData != null) {
				
				/* Reset i.*/
				i = Math.min(pText.length(), table.getTextMaxLength());
				if (i < hexEntryLength && pText.length() >= hexEntryLength) {
					i = hexEntryLength;
				}
				
			} else if (tableData == null) {
				i--;
			}

		}
		
		/* 
		 * If fixed length is used, check if there are any
		 * padding bytesleft to write.
		 */
		if (fixedLength > 0) {
			while (bytesWritten < fixedLength) {
				print(file, flData, currentFileOffset, stop.getValue());
				bytesWritten++;
				currentFileOffset += flData.length;
			}
		}
		
		/* Check if there were any missing characters, and append a warning. */
		if (missingChars.size() > 0) {
			err.add(new InsertError(missingChars, ErrorType.WARNING));
		}
		
		return err;
	}
	
	/**
	 * Writes data to a file from a specific offset. Writing is done
	 * by overwriting present bytes, and cannot expand the file size.
	 * @param file File to write to.
	 * @param data Data to write.
	 * @param offset Offset to write from.
	 * @param stop The file offset to stop printing the entire script at.
	 * @return True if bytes were written correctly within the
	 * 		   file length, otherwise false.
	 */
	private boolean print(RandomAccessFile file, byte[] data, long offset,
															  long stop) {
		/* No negative offsets. */
		if (offset < 0) {
			return false;
		}
		
		/* 
		 * If the byte data is larger than the script printing rage,
		 * only use a subset of it.
		 */
		if ((currentFileOffset + data.length) > stop) {
			data = ByteCalc.subArray(data, 0, (int) (stop - currentFileOffset));
		}
		
		try {
			/* Set position in file. */
			file.seek(offset);
					
			/* Write data */
			file.write(data);
			
			return true;
				
		} catch (IOException e) {
			return false;
		}				
	}
	
	/**
	 * Converts a hex-formatted string (<00> to <FF>) into
	 * a byte array.
	 * @param hexTag String of size 4, containing a hexadecimal value.
	 * @return Byte array of hexTags hexadecimal value, or null if no
	 * 		   match exists.
	 */
	private byte[] hexEntryToBytes(String hexTag) {
		Pattern pattern = Pattern.compile("<\\$([a-fA-F0-9]{2})>");
		Matcher matcher;
		String hex = null;
		byte[] ret;
		
		if (hexTag == null) {
			return null;
		} else if (hexTag.length() == 5) {
			matcher = pattern.matcher(hexTag);
			
			if (matcher.find()) {
				hex = hexTag.substring(2,4);
				
				try {
					ret = ByteCalc.longToByteArray(Long.parseLong(hex, 16));
					ret = ByteCalc.subArray(ret, ret.length - 1, ret.length);
					return ret;
				} catch (NumberFormatException e) {
					return null;
				}
			}
			
		}
		
		return null;
	}
	
	/**
	 * Checks if there are any critical error that should
	 * terminate insertion.
	 * @param errors List of errors.
	 * @return True if there are no errors of type ERRROR, otherwise false.
	 */
	private boolean validate(ArrayList<ErrorReport> errors) {
		for (ErrorReport e : errors) {
			if (e.getType() == ErrorType.ERROR) {
				return false;
			}
		}
		
		return true;
	}

}
